import { useMemoizedFn } from 'ahooks';
import { theme } from 'antd';
import { isNumber } from 'lodash';
import { useCallback, useEffect, useRef, useState } from 'react';
import type {
  Bounds,
  CoordinateTransform,
  FrequencyBand,
  GraphConfig,
  GraphState,
  GraphTransform,
  Margin,
  MarginInput,
  Point,
  TooltipState,
} from './types';

// Linear transform
export const linearTransform: CoordinateTransform = {
  transform: (
    value: number,
    min: number,
    max: number,
    start: number,
    end: number,
  ) => {
    return ((value - min) / (max - min)) * (end - start) + start;
  },
  inverse: (
    pixel: number,
    min: number,
    max: number,
    start: number,
    end: number,
  ) => {
    return ((pixel - start) / (end - start)) * (max - min) + min;
  },
};

// Log transform
export const logTransform: CoordinateTransform = {
  transform: (
    value: number,
    min: number,
    max: number,
    start: number,
    end: number,
  ) => {
    if (value === 0) return start;
    if (min === 0) {
      // Remain 10% space for 0 value
      const padding = (end - start) * 0.1;
      return (
        start +
        padding +
        (end - start - padding) * (Math.log(value) / Math.log(max))
      );
    }
    return (
      start +
      (end - start) *
        ((Math.log(value) - Math.log(min)) / (Math.log(max) - Math.log(min)))
    );
  },
  inverse: (
    pixel: number,
    min: number,
    max: number,
    start: number,
    end: number,
  ) => {
    if (pixel <= start) return 0;
    if (min === 0) {
      const padding = (end - start) * 0.1;
      if (pixel <= start + padding) return 0;
      return Math.exp(
        ((pixel - start - padding) / (end - start - padding)) * Math.log(max),
      );
    }
    return Math.exp(
      ((pixel - start) / (end - start)) * (Math.log(max) - Math.log(min)) +
        Math.log(min),
    );
  },
};

const DEFAULT_CONFIG: Required<GraphConfig> = {
  width: 800,
  height: 400,
  isDarkMode: false,
  margin: 40,
  xMin: 20.0,
  xMax: 20000.0,
  xInterval: false,
  xLabelPrefix: '',
  xLabelSuffix: '',
  xLabelSign: false,
  yMin: -18.0,
  yMax: 12.0,
  yInterval: 6.0,
  yLabelPrefix: '',
  yLabelSuffix: '',
  yLabelSign: false,
  xTransform: linearTransform,
  yTransform: linearTransform,
  lineWidth: 2,
  gridColor: '#d1d5db',
  gridColorDark: '#334155',
  axisColor: '#d1d5db',
  axisColorDark: '#334155',
  curveColor: '#FF0000',
  curveColorDark: '#FF6347',
  fontSize: 15,
  bandFontSize: 12,
  fontWeight: 'normal',
  fontFamily:
    'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace',
  labelColor: '#4b5563',
  labelColorDark: '#BBB',
  backgroundColor: 'transparent',
  backgroundColorDark: 'transparent',
  plotAreaBackgroundColor: 'transparent',
  plotAreaBackgroundColorDark: 'transparent',
  bandsBackgroundColor: '#f5f5f5',
  bandsBackgroundColorDark: '#334155',
  xLineColor: '#7C3AED',
  xLineColorDark: '#777777',
  highlightPointBorderWidth: 2,
  highlightPointRadius: 4,
  highlightPointBorderColor: '#FF0000',
  highlightPointBorderColorDark: '#FFA500',
  highlightPointBackgroundColor: 'white',
  highlightPointBackgroundColorDark: '#FFA500',
  tooltip: {
    enabled: false,
  },
  interaction: {
    snap: true,
    snapThreshold: 10,
  },
  frequencyBands: [
    { min: 20, max: 60, label: 'Sub Bass' },
    { min: 60, max: 250, label: 'Bass' },
    { min: 250, max: 500, label: 'Low Mids' },
    { min: 500, max: 2000, label: 'Mid Range' },
    { min: 2000, max: 4000, label: 'Upper Mids' },
    { min: 4000, max: 6000, label: 'Presence' },
    { min: 6000, max: 20000, label: 'Brilliance' },
  ],
};

export interface UseGraphOptions {
  canvasRef: React.RefObject<HTMLCanvasElement>;
  graphPoints: Point[];
  graphHighlightPoints?: Point[];
  graphConfig?: Partial<GraphConfig>;
  selectedPoint?: Point;
  addPoint?: (point: Point) => void;
  removePoint?: (point: Point) => void;
  onClick?: (point: Point) => void;
  onDragging?: (point: Point) => void;
  onUpdateSize?: (width: number, height: number) => void;
}

export function useGraph(options: UseGraphOptions) {
  const {
    canvasRef,
    graphPoints,
    graphHighlightPoints = [],
    selectedPoint,
    graphConfig = {},
    addPoint,
    removePoint,
    onClick,
    onDragging,
    onUpdateSize,
  } = options;

  // Graph config
  const config = {
    ...DEFAULT_CONFIG,
    ...graphConfig,
  };

  const { token } = theme.useToken();

  // Graph state
  const [state, setState] = useState<GraphState>({
    bounds: {
      xMin: config.xMin,
      xMax: config.xMax,
      yMin: config.yMin,
      yMax: config.yMax,
    },
    transform: {
      scale: { x: 1, y: 1 },
      translate: { x: 0, y: 0 },
    },
  });

  // Tooltip state
  const [tooltipState, setTooltipState] = useState<TooltipState>({
    visible: false,
    position: { x: 0, y: 0 },
    content: null,
  });

  // Highlight
  const [highlightXLine, setHighlightXLine] = useState<number | null>(null);
  const [activePoint, setActivePoint] = useState<Point | null>(null);
  const [snappingPoint, setSnappingPoint] = useState<Point | null>(null);

  // Interaction
  const [isMouseDown, setIsMouseDown] = useState(false);
  const [isDragging, setIsDragging] = useState(false);

  // Canvas Context
  const ctx = useRef<CanvasRenderingContext2D | null>(null);
  const rafId = useRef<number>(0);
  const isDrawing = useRef(false);

  const getPixelRatio = useCallback((context: CanvasRenderingContext2D) => {
    const backingStore =
      (context as any).backingStorePixelRatio ||
      (context as any).webkitBackingStorePixelRatio ||
      (context as any).mozBackingStorePixelRatio ||
      (context as any).msBackingStorePixelRatio ||
      (context as any).oBackingStorePixelRatio ||
      1;

    return (window.devicePixelRatio || 1) / backingStore;
  }, []);

  const setupHiDPICanvas = useCallback(
    (canvas: HTMLCanvasElement, width: number, height: number) => {
      if (!canvas || !ctx.current) return;

      const ratio = getPixelRatio(ctx.current);
      const displayWidth = Math.floor(width * ratio);
      const displayHeight = Math.floor(height * ratio);

      canvas.width = displayWidth;
      canvas.height = displayHeight;
      canvas.style.width = `${width}px`;
      canvas.style.height = `${height}px`;

      ctx.current.scale(ratio, ratio);
      ctx.current.imageSmoothingEnabled = true;
      ctx.current.imageSmoothingQuality = 'high';
    },
    [getPixelRatio],
  );

  const transform = useCallback(
    (point: Point): Point => {
      return transformCoordinates(point, config, state.bounds, state.transform);
    },
    [config, state.bounds, state.transform],
  );

  const inverseTransform = useCallback(
    (x: number, y: number): Point => {
      return inverseTransformCoordinates(
        x,
        y,
        config,
        state.bounds,
        state.transform,
      );
    },
    [config, state.bounds, state.transform],
  );

  const render = useMemoizedFn(() => {
    if (!ctx.current || !canvasRef.current) return;

    if (isDrawing.current) return;

    isDrawing.current = true;

    const draw = () => {
      if (!ctx.current) return;

      const {
        width,
        height,
        isDarkMode,
        backgroundColor,
        backgroundColorDark,
      } = config;

      // Clear canvas
      ctx.current.clearRect(0, 0, width, height);

      // Fill background
      ctx.current.fillStyle = isDarkMode
        ? backgroundColorDark
        : backgroundColor;
      ctx.current.fillRect(0, 0, width, height);

      // Draw grid
      drawGrid();

      // Draw axes
      drawAxes();

      // Draw curve
      drawCurve();

      // Draw xLines
      drawXLines();

      // Draw highlight points
      drawHighlightPoints();

      isDrawing.current = false;
    };

    cancelAnimationFrame(rafId.current);
    rafId.current = requestAnimationFrame(draw);
  });

  function drawGrid() {
    if (!ctx.current) return;

    const {
      width,
      height,
      margin,
      xInterval,
      yInterval,
      lineWidth,
      isDarkMode,
      plotAreaBackgroundColor,
      plotAreaBackgroundColorDark,
      gridColor,
      gridColorDark,
      frequencyBands,
    } = config;
    const { left, right, top, bottom } = convertMargin(margin);

    ctx.current.save();
    ctx.current.strokeStyle = isDarkMode ? gridColorDark : gridColor;
    ctx.current.lineWidth = lineWidth;

    ctx.current.save();
    ctx.current.fillStyle = isDarkMode
      ? plotAreaBackgroundColorDark
      : plotAreaBackgroundColor;
    ctx.current.fillRect(
      left,
      top,
      width - left - right,
      height - top - bottom,
    );
    ctx.current.restore();

    // Draw vertical grid lines
    if (xInterval) drawIntervalVerticalLines(xInterval);
    else drawDefaultVerticalLines();

    // Draw horizontal grid lines
    if (yInterval) drawIntervalHorizontalLines(yInterval);

    // Draw frequency bands
    drawFrequencyBands(frequencyBands);

    ctx.current.restore();
  }

  function drawFrequencyBands(bands: FrequencyBand[]) {
    if (!ctx.current || !bands.length) return;

    const {
      margin,
      bandFontSize,
      fontWeight,
      fontFamily,
      isDarkMode,
      labelColor,
      labelColorDark,
      bandsBackgroundColor,
      bandsBackgroundColorDark,
    } = config;

    const { top } = convertMargin(margin);
    const bandHeight = 30;
    const bandGap = 2;
    const bandY = top - bandHeight - 10;

    ctx.current.save();

    for (const range of bands) {
      // Calculate the start and end position of the block
      const startPoint = transform({ x: range.min, y: 0 });
      const endPoint = transform({ x: range.max, y: 0 });
      const blockWidth = endPoint.x - startPoint.x - bandGap;

      // Draw background
      ctx.current.beginPath();
      ctx.current.roundRect(
        startPoint.x + bandGap / 2,
        bandY,
        blockWidth,
        bandHeight,
        4, // Border radius
      );
      ctx.current.fillStyle = !isDarkMode
        ? bandsBackgroundColor
        : bandsBackgroundColorDark;
      ctx.current.fill();

      // Draw label
      ctx.current.font = `${fontWeight} ${bandFontSize}px ${fontFamily}`;

      ctx.current.fillStyle = isDarkMode ? labelColorDark : labelColor;
      ctx.current.textAlign = 'center';
      ctx.current.textBaseline = 'middle';
      ctx.current.fillText(
        range.label,
        startPoint.x + blockWidth / 2 + bandGap / 2,
        bandY + bandHeight / 2,
      );
    }

    ctx.current.restore();
  }

  function drawIntervalVerticalLines(interval: number) {
    if (!ctx.current) return;

    const {
      height,
      margin,
      fontSize,
      fontWeight,
      fontFamily,
      isDarkMode,
      labelColor,
      labelColorDark,
      xLabelPrefix,
      xLabelSuffix,
      xLabelSign,
    } = config;
    const { bounds } = state;
    const { xMin } = bounds;
    const { top, bottom } = convertMargin(margin);

    ctx.current.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
    ctx.current.fillStyle = isDarkMode ? labelColorDark : labelColor;
    ctx.current.textAlign = 'center';
    ctx.current.textBaseline = 'top';

    for (let x = bounds.xMin; x <= bounds.xMax; x += interval) {
      const { x: transformedX } = transform({ x, y: 0 });

      if (x > xMin) {
        // Draw vertical grid lines
        ctx.current.beginPath();
        ctx.current.moveTo(transformedX, top);
        ctx.current.lineTo(transformedX, height - bottom);
        ctx.current.stroke();
      }

      // Draw x-axis labels
      const sign = xLabelSign ? (x > 0 ? '+' : '') : '';
      const label = `${xLabelPrefix}${sign}${x}${xLabelSuffix}`;
      ctx.current.fillText(label, transformedX, height - bottom + 8);
    }
  }

  function drawDefaultVerticalLines() {
    if (!ctx.current) return;

    const {
      width,
      height,
      margin,
      fontSize,
      fontWeight,
      fontFamily,
      isDarkMode,
      labelColor,
      labelColorDark,
      xLabelPrefix,
      xLabelSuffix,
      xLabelSign,
    } = config;
    const { bounds } = state;
    const { xMin, xMax } = bounds;

    const { left, right, top, bottom } = convertMargin(margin);

    ctx.current.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
    ctx.current.fillStyle = isDarkMode ? labelColorDark : labelColor;
    ctx.current.textAlign = 'center';
    ctx.current.textBaseline = 'top';

    const innerWidth = width - left - right;
    const min = 10;
    let exponent = Math.log10(min);
    let mantissa = Math.ceil(min / 10.0 ** exponent);
    let value = 0;

    while (value <= xMax) {
      const transformedX =
        left +
        innerWidth *
          ((Math.log(value) - Math.log(xMin)) /
            (Math.log(xMax) - Math.log(xMin)));

      if (value > 0) {
        // Draw vertical grid lines
        ctx.current.beginPath();
        ctx.current.moveTo(transformedX, top);
        ctx.current.lineTo(transformedX, height - bottom);
        ctx.current.stroke();
      }

      // Add label
      if (mantissa === 1 || mantissa === 2 || mantissa === 5) {
        const text =
          value < 1000 ? value.toFixed(0) : `${(value / 1000).toFixed(0)}K`;

        const sign = xLabelSign ? (value > 0 ? '+' : '') : '';
        const label = `${xLabelPrefix}${sign}${text}${xLabelSuffix}`;
        ctx.current.fillText(label, transformedX, height - bottom + 10);
      }

      mantissa += 1;
      if (mantissa >= 10) {
        mantissa = 1;
        exponent += 1;
      }

      value = Math.floor(mantissa * 10.0 ** exponent);
    }
  }

  function drawIntervalHorizontalLines(interval: number) {
    if (!ctx.current) return;

    const {
      width,
      margin,
      lineWidth,
      fontSize,
      fontWeight,
      fontFamily,
      isDarkMode,
      labelColor,
      labelColorDark,
      yLabelPrefix,
      yLabelSuffix,
      yLabelSign,
    } = config;
    const { bounds } = state;
    const { left, right } = convertMargin(margin);

    ctx.current.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
    ctx.current.fillStyle = isDarkMode ? labelColorDark : labelColor;
    ctx.current.textAlign = 'right';
    ctx.current.textBaseline = 'middle';

    // Start from 0 and extend in both positive and negative directions
    for (
      let y = 0;
      y <= Math.max(Math.abs(bounds.yMin), Math.abs(bounds.yMax));
      y += interval
    ) {
      // Draw positive values
      if (y <= bounds.yMax) {
        const { y: transformedY } = transform({ x: 0, y });

        // Draw horizontal grid lines
        ctx.current.save();
        if (y === 0) {
          ctx.current.setLineDash([]);
        } else {
          ctx.current.setLineDash([6, 6]);
        }
        ctx.current.beginPath();
        ctx.current.moveTo(left, transformedY);
        ctx.current.lineTo(width - right + lineWidth / 2, transformedY);
        ctx.current.stroke();
        ctx.current.restore();

        // Draw y-axis labels
        const sign = yLabelSign ? '+' : '';
        const label = `${yLabelPrefix}${sign}${y}${yLabelSuffix}`;
        ctx.current.fillText(label, left - 8, transformedY);
      }

      // Draw negative values (except 0)
      if (y > 0 && -y >= bounds.yMin) {
        const { y: transformedY } = transform({ x: 0, y: -y });

        // Draw horizontal grid lines
        ctx.current.save();
        ctx.current.setLineDash([6, 6]);
        ctx.current.beginPath();
        ctx.current.moveTo(left, transformedY);
        ctx.current.lineTo(width - right + lineWidth / 2, transformedY);
        ctx.current.stroke();
        ctx.current.restore();

        // Draw y-axis labels
        const label = `${yLabelPrefix}-${y}${yLabelSuffix}`;
        ctx.current.fillText(label, left - 8, transformedY);
      }
    }
  }

  function drawAxes() {
    if (!ctx.current) return;

    const {
      width,
      height,
      isDarkMode,
      margin = 0,
      lineWidth,
      axisColor,
      axisColorDark,
      fontSize,
      fontWeight,
      fontFamily,
      labelColor,
    } = config;

    const { top, right, bottom, left } = convertMargin(margin);

    ctx.current.save();
    ctx.current.strokeStyle = isDarkMode ? axisColorDark : axisColor;
    ctx.current.lineWidth = lineWidth;
    ctx.current.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
    ctx.current.fillStyle = labelColor;

    const axisOffset = ctx.current.lineWidth / 2;

    // Draw x-axis
    ctx.current.beginPath();
    ctx.current.moveTo(left, height - bottom + axisOffset);
    ctx.current.lineTo(
      width - right + axisOffset,
      height - bottom + axisOffset,
    );
    ctx.current.stroke();

    // Draw x-axis top
    ctx.current.beginPath();
    ctx.current.moveTo(left, bottom);
    ctx.current.lineTo(width - right + axisOffset, bottom);
    ctx.current.stroke();

    // Draw y-axis
    ctx.current.beginPath();
    ctx.current.moveTo(left, height - bottom + ctx.current.lineWidth);
    ctx.current.lineTo(left, top - axisOffset);
    ctx.current.stroke();

    // Draw y-axis right
    ctx.current.beginPath();
    ctx.current.moveTo(width - right, height - bottom + ctx.current.lineWidth);
    ctx.current.lineTo(width - right, top - axisOffset);
    ctx.current.stroke();

    ctx.current.restore();
  }

  function drawCurve() {
    if (!ctx.current || !graphPoints.length) return;

    const { width, height, isDarkMode, curveColor, curveColorDark, margin } =
      config;
    const { left, right, top, bottom } = convertMargin(margin);

    ctx.current.save();

    // Set clipping area
    ctx.current.beginPath();
    ctx.current.rect(left, top, width - left - right, height - top - bottom);
    ctx.current.clip();

    // Draw curve
    ctx.current.strokeStyle = isDarkMode ? curveColorDark : curveColor;
    ctx.current.lineWidth = 3;
    ctx.current.beginPath();

    const firstPoint = transform(graphPoints[0]);
    ctx.current.moveTo(firstPoint.x, firstPoint.y);

    for (let i = 1; i < graphPoints.length; i++) {
      const point = transform(graphPoints[i]);
      ctx.current.lineTo(point.x, point.y);
    }

    ctx.current.stroke();
    ctx.current.restore();
  }

  function drawXLines() {
    if (!ctx.current) return;

    const {
      height,
      margin = 0,
      isDarkMode,
      xLineColor,
      xLineColorDark,
    } = config;
    const dashedLine = [6, 6];

    const { top, bottom } = convertMargin(margin);

    ctx.current.save();
    ctx.current.lineWidth = 2;

    // Draw xLine
    if (highlightXLine !== null) {
      const { x } = transform({ x: highlightXLine, y: 0 });
      if (isDragging) ctx.current.setLineDash([]);
      else ctx.current.setLineDash(dashedLine);
      ctx.current.strokeStyle = isDarkMode ? xLineColorDark : xLineColor;

      ctx.current.beginPath();
      ctx.current.moveTo(x, top);
      ctx.current.lineTo(x, height - bottom);
      ctx.current.stroke();
    }

    ctx.current.restore();
  }

  function drawHighlightPoints() {
    if (!ctx.current || !graphHighlightPoints.length) return;

    const {
      width,
      height,
      margin,
      isDarkMode,
      highlightPointRadius,
      highlightPointBorderWidth,
      highlightPointBorderColor,
      highlightPointBorderColorDark,
      highlightPointBackgroundColor,
      highlightPointBackgroundColorDark,
    } = config;
    const { left, right, top, bottom } = convertMargin(margin);

    const borderColor = isDarkMode
      ? highlightPointBorderColorDark
      : highlightPointBorderColor;
    const backgroundColor = isDarkMode
      ? highlightPointBackgroundColorDark
      : highlightPointBackgroundColor;

    ctx.current.save();

    // Set clipping area
    ctx.current.beginPath();
    ctx.current.rect(left, top, width - left - right, height - top - bottom);
    ctx.current.clip();

    ctx.current.fillStyle = backgroundColor;
    ctx.current.strokeStyle = borderColor;
    ctx.current.lineWidth = highlightPointBorderWidth;

    for (const point of graphHighlightPoints) {
      const { x, y } = transform(point);

      const isScale = activePoint?.x === point.x && !isMouseDown;
      let radius = (isScale ? 1.5 : 1) * highlightPointRadius;

      if (selectedPoint && selectedPoint.x === point.x) {
        radius = 1.5 * highlightPointRadius;
        ctx.current.strokeStyle = backgroundColor;
        ctx.current.fillStyle = '#4096FF';
      } else {
        ctx.current.strokeStyle = borderColor;
        ctx.current.fillStyle = backgroundColor;
      }

      ctx.current.beginPath();
      ctx.current.arc(x, y, radius, 0, Math.PI * 2);
      ctx.current.fill();
      ctx.current.stroke();
    }

    ctx.current.restore();
  }

  function findNearestHighlightPoint(
    x: number,
    threshold: number,
  ): Point | null {
    if (!graphHighlightPoints.length) {
      return null;
    }

    let _point: Point | null = null;
    for (const point of graphHighlightPoints) {
      const distance = Math.abs(point.x - x);

      if (distance < threshold) {
        _point = point;
        break;
      }
    }

    // return _point || selectedPoint || null;
    return _point;
  }

  function isInOperableArea(x: number, y: number): boolean {
    const scale = state.transform.scale.x;
    const { width, height, margin = 0 } = config;
    const { top, right, bottom, left } = convertMargin(margin);

    const scaledMargin: Margin = {
      top: top * scale,
      right: right * scale,
      bottom: bottom * scale,
      left: left * scale,
    };

    return (
      x >= scaledMargin.left &&
      x <= width - scaledMargin.right &&
      y >= scaledMargin.top &&
      y <= height - scaledMargin.bottom
    );
  }

  const handleMouseDown = useMemoizedFn((event: MouseEvent) => {
    if (!canvasRef.current) return;

    setIsMouseDown(true);

    const rect = canvasRef.current.getBoundingClientRect();
    const point = inverseTransform(
      event.clientX - rect.left,
      event.clientY - rect.top,
    );

    if (!isInOperableArea(event.clientX - rect.left, event.clientY - rect.top))
      return;

    if (snappingPoint) {
      setHighlightXLine(snappingPoint.x);
      onClick?.(snappingPoint);
      setIsDragging(true);
    } else {
      setHighlightXLine(null);
      setIsDragging(false);
      addPoint?.(point);
    }

    render();
  });

  const handleMouseMove = useMemoizedFn((event: MouseEvent) => {
    if (!canvasRef.current) return;

    const rect = canvasRef.current.getBoundingClientRect();
    const point = inverseTransform(
      event.clientX - rect.left,
      event.clientY - rect.top,
    );

    if (
      !isInOperableArea(event.clientX - rect.left, event.clientY - rect.top)
    ) {
      handleMouseLeave();
      return;
    }

    if (isDragging) {
      setHighlightXLine(point.x);
      showTooltip(point);
      onDragging?.(point);
      render();
      return;
    }

    const defaultThreshold = config.interaction?.snapThreshold || 10;
    const threshold = isNumber(defaultThreshold)
      ? defaultThreshold
      : defaultThreshold(point.x);
    const nearestPoint = findNearestHighlightPoint(point.x, threshold);

    if (nearestPoint) {
      setActivePoint(nearestPoint);
      setSnappingPoint(nearestPoint);
      setHighlightXLine(nearestPoint.x);
      showTooltip(nearestPoint);
    } else {
      setActivePoint(null);
      setSnappingPoint(null);
      setHighlightXLine(point.x);
      hideTooltip();
    }

    render();
  });

  const handleMouseUp = useMemoizedFn(() => {
    if (!canvasRef.current) return;

    setIsMouseDown(false);
    setIsDragging(false);
    render();
  });

  const handleMouseLeave = useMemoizedFn(() => {
    if (!canvasRef.current) return;

    setSnappingPoint(null);
    setHighlightXLine(null);
    setIsMouseDown(false);
    setIsDragging(false);
    render();
  });

  const handleKeyDown = useMemoizedFn((event: KeyboardEvent) => {
    if (!canvasRef.current) return;

    if (event.key === 'Delete' && activePoint) {
      removePoint?.(activePoint);
      hideTooltip();
    }
  });

  function showTooltip(point: Point) {
    if (!config.tooltip.enabled) return;

    setTooltipState({
      visible: true,
      position: transform(point),
      content: {
        x: point.x,
        y: point.y,
        label: point.label,
      },
    });
  }

  function hideTooltip() {
    setTooltipState({
      visible: false,
      position: { x: 0, y: 0 },
      content: null,
    });
  }

  function highlight(x: number | null) {
    setHighlightXLine(x);
    render();
  }

  function getContainerSize(canvas: HTMLCanvasElement) {
    const container = canvas.parentElement ?? canvas;

    return {
      width: container.clientWidth,
      height: container.clientHeight,
    };
  }

  useEffect(() => {
    if (!canvasRef.current) return;

    ctx.current = canvasRef.current.getContext('2d');
    render();

    const handleResize = () => {
      if (!canvasRef.current) return;
      const { width, height } = getContainerSize(canvasRef.current);

      config.width = width;
      config.height = height;

      setupHiDPICanvas(canvasRef.current, width, height);
      onUpdateSize?.(width, height);
      render();
    };

    const handleResizeThrottled = throttle(handleResize, 50);

    setTimeout(() => {
      handleResize();
    }, 0);

    window.addEventListener('resize', handleResizeThrottled);
    canvasRef.current.addEventListener('mousedown', handleMouseDown);
    canvasRef.current.addEventListener('mousemove', handleMouseMove);
    canvasRef.current.addEventListener('mouseup', handleMouseUp);
    canvasRef.current.addEventListener('mouseleave', handleMouseLeave);
    window.addEventListener('keydown', handleKeyDown);

    return () => {
      cancelAnimationFrame(rafId.current);
      window.removeEventListener('resize', handleResizeThrottled);
      if (canvasRef.current) {
        canvasRef.current.removeEventListener('mousedown', handleMouseDown);
        canvasRef.current.removeEventListener('mousemove', handleMouseMove);
        canvasRef.current.removeEventListener('mouseup', handleMouseUp);
        canvasRef.current.removeEventListener('mouseleave', handleMouseLeave);
      }
      window.removeEventListener('keydown', handleKeyDown);
    };
  }, []);

  useEffect(() => {
    render();
  }, [graphPoints, graphHighlightPoints]);

  return {
    tooltipVisible: tooltipState.visible,
    tooltipPosition: tooltipState.position,
    tooltipContent: tooltipState.content,

    render,
    highlight: (x: number | null) => {
      setHighlightXLine(x);
      render();
    },
  };
}

export function transformCoordinates(
  point: Point,
  config: GraphConfig,
  bounds: Bounds,
  transform: GraphTransform,
): Point {
  const { width, height, margin = 0 } = config;
  const { xMin, xMax, yMin, yMax } = bounds;
  const { top, right, bottom, left } = convertMargin(margin);

  const xTransform = config.xTransform || linearTransform;
  const yTransform = config.yTransform || linearTransform;

  const x =
    xTransform.transform(point.x, xMin, xMax, left, width - right) *
      transform.scale.x +
    transform.translate.x;

  const y =
    height -
    yTransform.transform(point.y, yMin, yMax, top, height - bottom) *
      transform.scale.y +
    transform.translate.y;

  return { x, y };
}

export function inverseTransformCoordinates(
  x: number,
  y: number,
  config: GraphConfig,
  bounds: Bounds,
  transform: GraphTransform,
): Point {
  const { width, height, margin = 0 } = config;
  const { xMin, xMax, yMin, yMax } = bounds;
  const { top, right, bottom, left } = convertMargin(margin);

  const xTransform = config.xTransform || linearTransform;
  const yTransform = config.yTransform || linearTransform;

  const pointX = xTransform.inverse(
    (x - transform.translate.x) / transform.scale.x,
    xMin,
    xMax,
    left,
    width - right,
  );

  const pointY = yTransform.inverse(
    height - (y - transform.translate.y) / transform.scale.y,
    yMin,
    yMax,
    top,
    height - bottom,
  );

  return { x: pointX, y: pointY };
}

export function convertMargin(margin: MarginInput): Margin {
  if (typeof margin === 'number') {
    return {
      top: margin,
      right: margin,
      bottom: margin,
      left: margin,
    };
  } else {
    return {
      top: margin.top || 0,
      right: margin.right || 0,
      bottom: margin.bottom || 0,
      left: margin.left || 0,
    };
  }
}

function throttle<T extends (...args: any[]) => any>(
  func: T,
  limit: number,
): (...args: Parameters<T>) => void {
  let inThrottle: boolean;
  let lastFunc: ReturnType<typeof setTimeout>;
  let lastRan: number;

  return function (...args: Parameters<T>): void {
    if (!inThrottle) {
      func.apply(this, args);
      lastRan = Date.now();
      inThrottle = true;
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(
        () => {
          if (Date.now() - lastRan >= limit) {
            func.apply(this, args);
            lastRan = Date.now();
          }
        },
        limit - (Date.now() - lastRan),
      );
    }
  };
}

function getContainerSize(canvas: HTMLCanvasElement) {
  const container = canvas.parentElement ?? canvas;
  return {
    width: container.clientWidth,
    height: container.clientHeight,
  };
}

function isInOperableArea(
  x: number,
  y: number,
  config: GraphConfig,
  state: GraphState,
): boolean {
  const scale = state.transform.scale.x;
  const { width, height, margin = 0 } = config;
  const { top, right, bottom, left } = convertMargin(margin);

  const scaledMargin: Margin = {
    top: top * scale,
    right: right * scale,
    bottom: bottom * scale,
    left: left * scale,
  };

  return (
    x >= scaledMargin.left &&
    x <= width - scaledMargin.right &&
    y >= scaledMargin.top &&
    y <= height - scaledMargin.bottom
  );
}

export function linearInterpolationValue(
  point1: Point | undefined,
  point2: Point | undefined,
  x: number,
): number {
  if (!point1 || !point2) return 0;

  if (!point1) {
    return point2.y;
  }

  if (!point2) {
    return point1.y;
  }

  const k = (point2.y - point1.y) / (point2.x - point1.x);
  const b = point1.y - k * point1.x;
  return k * x + b;
}
